/*
* Copyright 2012 david gonzalez.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.activecq.samples.replication.impl;
import com.day.cq.commons.jcr.JcrConstants;
import com.day.cq.replication.Agent;
import com.day.cq.replication.AgentFilter;
import com.day.cq.replication.AgentManager;
import com.day.cq.replication.ReplicationActionType;
import com.day.cq.replication.ReplicationException;
import com.day.cq.replication.ReplicationOptions;
import com.day.cq.replication.Replicator;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.api.SlingConstants;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.commons.osgi.PropertiesUtil;
import org.apache.sling.event.EventUtil;
import org.apache.sling.event.jobs.JobProcessor;
import org.apache.sling.event.jobs.JobUtil;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
import org.osgi.service.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.ValueFormatException;
import javax.jcr.lock.LockException;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.version.VersionException;
import java.util.Calendar;
import java.util.Date;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Component(label="Samples - Reverse Replicator",
description="Provides a simple mechanism for reverse replicating nodes. A Reverse replicator agent must be setup on the Server.",
configurationFactory=true,
immediate=true,
metatype=true)
@Properties ({
@Property(
name="service.vendor",
value="ActiveCQ"
),
@Property(
label="Sling Event Topics",
name="event.topics",
description="Sling Events to listen to via this Sling Event Handler." +
"Values are limited to: " +
"org/apache/sling/api/resource/Resource/ADDED, " +
"org/apache/sling/api/resource/Resource/CHANGED, " +
"org/apache/sling/api/resource/Resource/REMOVED",
value={
org.apache.sling.api.SlingConstants.TOPIC_RESOURCE_ADDED,
org.apache.sling.api.SlingConstants.TOPIC_RESOURCE_CHANGED,
org.apache.sling.api.SlingConstants.TOPIC_RESOURCE_REMOVED
}
)
})
@Service
public class ReverseReplicatorImpl implements JobProcessor, EventHandler {
public static final String SLING_TOPIC_ADDED = "org/apache/sling/api/resource/Resource/ADDED";
public static final String SLING_TOPIC_CHANGED = "org/apache/sling/api/resource/Resource/CHANGED";
public static final String SLING_TOPIC_REMOVED = "org/apache/sling/api/resource/Resource/REMOVED";
@Reference
AgentManager agentManager;
@Reference
private ResourceResolverFactory resourceResolverFactory;
@Reference
private Replicator replicator;
@Reference
private EventAdmin eventAdmin;
//private ResourceResolver adminResourceResolver;
//private Session adminSession;
protected final Logger log = LoggerFactory.getLogger(this.getClass());
private static final boolean DEFAULT_ENABLED = true;
private boolean enabled = DEFAULT_ENABLED;
@Property(label="Enable",
description="Enables this reverse replication configuration.",
boolValue=DEFAULT_ENABLED)
private static final String PROP_ENABLED = "prop.enabled";
private static final boolean DEFAULT_SYNCHRONOUS = false;
private boolean sychronous = DEFAULT_SYNCHRONOUS;
@Property(label="Synchronous Replication",
description="Should the replication be done synchronous or asynchronous? The default is 'false'.",
boolValue=DEFAULT_SYNCHRONOUS)
private static final String PROP_SYNCHRONOUS = "prop.synchronous";
private static final boolean DEFAULT_SUPRESS_STATUS_UPDATE = true;
private boolean suppressStatusUpdate = DEFAULT_SUPRESS_STATUS_UPDATE;
@Property(label="Supress Status Update",
description="If set to true the replication will not update the replication status properties after a replication. Default is 'true'.",
boolValue=DEFAULT_SUPRESS_STATUS_UPDATE)
private static final String PROP_SUPRESS_STATUS_UPDATE = "prop.supress-status-update";
private static final boolean DEFAULT_SUPRESS_VERSIONING = true;
private boolean supressVersioning = DEFAULT_SUPRESS_VERSIONING;
@Property(label="Supress Versioning",
description="If set to true the replication will not trigger implicit versioning. Default is 'true'",
boolValue=DEFAULT_SUPRESS_VERSIONING)
private static final String PROP_SUPRESS_VERSIONING = "prop.supress-versioning";
private static final String[] DEFAULT_PATHS = { "/path/to/replicate" };
private String[] paths = DEFAULT_PATHS;
@Property(label="Paths to replicate",
description="JCR paths to listen on.",
cardinality=Integer.MAX_VALUE,
value={"/path/to/replicate"})
private static final String PROP_PATHS = "prop.paths";
private static final String[] DEFAULT_PRIMARY_TYPES = { };
private String[] primaryTypes = DEFAULT_PRIMARY_TYPES;
@Property(label="Primary Node Types",
description="jcr:primaryType's to reverse replciate. Leave blank to disable this filter.",
cardinality=Integer.MAX_VALUE,
value={""})
private static final String PROP_PRIMARY_TYPES = "prop.primary-types";
private static final String[] DEFAULT_PROPERTY_MATCHES = { "cq:distribute=true" };
private Map<String, String> propertyMatches = new HashMap<String, String>();
@Property(label="Primary Node Types to Replicate.",
description="Format is: <property>=<value>. Leave blank to disable this filter.",
cardinality=Integer.MAX_VALUE,
value={"cq:distribute=true"})
private static final String PROP_PROPERTY_MATCHES = "prop.property-matches";
private static final String[] DEFAULT_PATH_BLACKLIST = { };
private String[] pathBlacklist = DEFAULT_PATH_BLACKLIST;
@Property(label="Blacklist Regex",
description="",
cardinality=Integer.MAX_VALUE,
value={})
private static final String PROP_PATH_BLACKLIST = "prop.path-blacklist";
private static final String[] DEFAULT_PATH_WHITELIST = { };
private String[] pathWhitelist = DEFAULT_PATH_WHITELIST;
@Property(label="Whitelist Regex",
description="",
cardinality=Integer.MAX_VALUE,
value={})
private static final String PROP_PATH_WHITELIST = "prop.path-whitelist";
/**
* Sling Event Handling
*
* handleEvent and process implement the Sling Eventing which handles
* syncing changes from the JCR/CRX to the FileSystem via: - VAULT UPDATE
*
*/
public void handleEvent(Event event) {
if (EventUtil.isLocal(event)) {
JobUtil.processJob(event, this);
}
}
public boolean process(Event event) {
if(!enabled) { return false; }
ResourceResolver adminResourceResolver = null;
try {
adminResourceResolver = resourceResolverFactory.getAdministrativeResourceResolver(null);
final String resourcePath = (String) event.getProperty("path");
for (final String path : paths) {
if (StringUtils.startsWithIgnoreCase(resourcePath, path)) {
log.debug("Processing Reverse Replication Event for: " + resourcePath);
Resource resource = adminResourceResolver.resolve(resourcePath);
if(resource == null) { continue; }
log.debug("Primary Type: " + hasValidPrimaryType(resource));
log.debug("Property: " + hasValidProperty(resource));
log.debug("Whitelist: " + isWhitelisted(resource));
log.debug("Not Blacklist: " + !isBlacklisted(resource));
log.debug("Event: " + event.getTopic());
log.debug("is Delete: " + isDeleteEvent(event));
if(!isDeleteEvent(event)) {
if(!hasValidPrimaryType(resource) || !hasValidProperty(resource)) { continue; }
}
if(!isWhitelisted(resource) || isBlacklisted(resource)) { continue; }
if(!shouldReplicate(resource, event)) { continue; }
try {
replicate(resource, event);
log.debug("*** REPLICATION KICKED OFF ***");
break;
} catch (ReplicationException ex) {
log.debug("*** REPLICATION FAILED ***");
log.debug(ex.getMessage());
}
}
}
} catch(Exception ex) {
log.debug("*** REPLICATION FAILED : REPOSITORY EXCEPTION GETTING ADMIN RESOURCE RESOLVER ***");
log.debug(ex.getMessage());
} finally {
if (adminResourceResolver != null) {
adminResourceResolver.close();
}
}
return true;
}
private boolean shouldReplicate(final Resource resource, Event event) {
if(StringUtils.equals(event.getTopic(), SlingConstants.TOPIC_RESOURCE_CHANGED)) {
ValueMap properties = resource.adaptTo(ValueMap.class);
final Date lastModified = properties.get(JcrConstants.JCR_LASTMODIFIED, Date.class);
final Date lastReplicated = properties.get("cq:lastReplicated", Date.class);
if(lastReplicated == null) { return true; }
if(lastModified == null) { setLastModified(resource); }
log.debug("LM " + lastModified.getTime() + " >= LR " + lastReplicated.getTime() + " => " + lastModified.after(lastReplicated));
// Last Modified must be >= Last Replicated
return lastModified.after(lastReplicated);
} else {
return true;
}
}
private void setLastModified(final Resource resource) {
try {
Calendar now = Calendar.getInstance();
Node node = resource.adaptTo(Node.class);
node.setProperty(JcrConstants.JCR_LASTMODIFIED, now);
node.setProperty(JcrConstants.JCR_LAST_MODIFIED_BY, resource.getResourceResolver().getUserID());
node.getSession().save();
} catch (ValueFormatException ex) {
java.util.logging.Logger.getLogger(ReverseReplicatorImpl.class.getName()).log(Level.SEVERE, null, ex);
} catch (VersionException ex) {
java.util.logging.Logger.getLogger(ReverseReplicatorImpl.class.getName()).log(Level.SEVERE, null, ex);
} catch (LockException ex) {
java.util.logging.Logger.getLogger(ReverseReplicatorImpl.class.getName()).log(Level.SEVERE, null, ex);
} catch (ConstraintViolationException ex) {
java.util.logging.Logger.getLogger(ReverseReplicatorImpl.class.getName()).log(Level.SEVERE, null, ex);
} catch (RepositoryException ex) {
java.util.logging.Logger.getLogger(ReverseReplicatorImpl.class.getName()).log(Level.SEVERE, null, ex);
}
}
private boolean isWhitelisted(final Resource resource) {
if(pathWhitelist.length <= 0) { return true; }
for(String regex : pathWhitelist) {
if(StringUtils.stripToNull(regex) == null) { continue; }
log.debug("White : " + regex + " vs " + resource.getPath());
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(resource.getPath());
if(m.find()) {
return true;
}
}
return false;
}
private boolean isBlacklisted(Resource resource) {
if(pathBlacklist.length <= 0) { return false; }
for(String regex : pathBlacklist) {
if(StringUtils.stripToNull(regex) == null) { continue; }
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(resource.getPath());
if(m.find()) {
return true;
}
}
return false;
}
private boolean hasValidPrimaryType(Resource resource) {
if(primaryTypes.length <= 0) { return true; }
ValueMap properties = resource.adaptTo(ValueMap.class);
for(final String primaryType : primaryTypes) {
if(StringUtils.stripToNull(primaryType) == null) { continue; }
String tmp = properties.get(JcrConstants.JCR_PRIMARYTYPE, String.class);
log.debug("PT : " + tmp + " vs " + primaryType);
if(StringUtils.equals(primaryType, tmp)) {
return true;
}
}
return false;
}
private boolean hasValidProperty(Resource resource) {
if(propertyMatches.isEmpty()) { return true; }
// Removal event
if(resource.adaptTo(Node.class) == null) { return true; }
ValueMap properties = resource.adaptTo(ValueMap.class);
for(final String property : propertyMatches.keySet()) {
if(StringUtils.stripToNull(property) == null) { continue; }
final String value = (String)propertyMatches.get(property);
if(properties.containsKey(property)) {
String tmp = properties.get(property, String.class);
if(StringUtils.equals(value, tmp)) {
log.debug("Expected(" + property + ": " + value + ") -> Actual(" + property + ": " + tmp + ")");
return true;
}
}
}
return false;
}
private void replicate(final Resource resource, final Event event) throws ReplicationException {
if(resource == null) {
return;
}
final ReplicationOptions replicationOptions = new ReplicationOptions();
final Session adminSession = resource.getResourceResolver().adaptTo(Session.class);
final String revision = (String) resource.getResourceMetadata().get("resourceVersion");
if(revision != null) {
replicationOptions.setRevision(revision);
}
replicationOptions.setFilter(DISTRIBUTE_AGENT_FILTER);
replicationOptions.setSynchronous(sychronous);
replicationOptions.setSuppressStatusUpdate(suppressStatusUpdate);
replicationOptions.setSuppressVersions(supressVersioning);
if(canReplicate(null, resource.getPath())) {//adminResourceResolver.adaptTo(User.class), resource.getPath())) {
replicator.replicate(adminSession, getReplicationActionType(event), resource.getPath(), replicationOptions);
} else {
final String path = resource.getPath();
log.error((new StringBuilder()).append(adminSession.getUserID()).append(" is not allowed to replicate this page/asset ").append(path).append(". Issuing request for 'replication'").toString());
final Dictionary properties = new Hashtable<String, Object>();
properties.put("path", path);
properties.put("replicationType", getReplicationActionType(event));
final Event activationEvent = new Event("com/day/cq/wcm/workflow/req/for/activation", properties);
eventAdmin.sendEvent(activationEvent);
}
}
protected boolean canReplicate(Object user, String path) {
// Uses Admin Session; Can always replicat.
return true;
// return user.hasPermissionOn("wcm/core/privileges/replicate", path);
}
protected ReplicationActionType getReplicationActionType(Event event) {
if(isDeleteEvent(event)) {
return ReplicationActionType.DELETE;
} else {
return ReplicationActionType.ACTIVATE;
}
}
protected boolean isDeleteEvent(Event event) {
return SlingConstants.TOPIC_RESOURCE_REMOVED.toString().equals( event.getTopic()) ||
SLING_TOPIC_REMOVED.equals(event.getTopic());
}
protected void activate(ComponentContext componentContext) {
Dictionary properties = componentContext.getProperties();
enabled = PropertiesUtil.toBoolean(properties.get(PROP_ENABLED), DEFAULT_ENABLED);
log.debug("Enabled: " + enabled);
sychronous = PropertiesUtil.toBoolean(properties.get(PROP_SYNCHRONOUS), DEFAULT_SYNCHRONOUS);
suppressStatusUpdate = PropertiesUtil.toBoolean(properties.get(PROP_SUPRESS_STATUS_UPDATE), DEFAULT_SUPRESS_STATUS_UPDATE);
supressVersioning = PropertiesUtil.toBoolean(properties.get(PROP_SUPRESS_VERSIONING), DEFAULT_SUPRESS_VERSIONING);
paths = PropertiesUtil.toStringArray(properties.get(PROP_PATHS), DEFAULT_PATHS);
paths = (String[]) ArrayUtils.removeElement(paths, "");
pathWhitelist = PropertiesUtil.toStringArray(properties.get(PROP_PATH_WHITELIST), DEFAULT_PATH_WHITELIST);
pathWhitelist = (String[]) ArrayUtils.removeElement(pathWhitelist, "");
pathBlacklist = PropertiesUtil.toStringArray(properties.get(PROP_PATH_BLACKLIST), DEFAULT_PATH_BLACKLIST);
pathBlacklist = (String[]) ArrayUtils.removeElement(pathBlacklist, "");
primaryTypes = PropertiesUtil.toStringArray(properties.get(PROP_PRIMARY_TYPES), DEFAULT_PRIMARY_TYPES);
primaryTypes = (String[]) ArrayUtils.removeElement(primaryTypes, "");
String[] tmp = PropertiesUtil.toStringArray(properties.get(PROP_PROPERTY_MATCHES), DEFAULT_PROPERTY_MATCHES);
tmp = (String[]) ArrayUtils.removeElement(tmp, "");
for(final String t : tmp) {
String[] s = StringUtils.split(t, '=');
if(s == null || s.length != 2) { continue; }
propertyMatches.put(s[0], s[1]);
}
}
protected void deactivate(ComponentContext componentContext) {
}
private static final AgentFilter DISTRIBUTE_AGENT_FILTER = new AgentFilter() {
public boolean isIncluded(Agent agent) {
return agent.getConfiguration().isTriggeredOnDistribute();
}
};
}